出处:倔金
原作者:寅时码
React 团队一到前端圈,所有敲代码的人便都看着他笑,有的叫道:“React,你又悄摸摸搞出几个极其拧巴的缝合怪 API!”
他不回答,对柜里说:“加两套重型编译器,要一碟 useEffectEvent。”便排出几个难懂的心智模型
他们又故意的高声嚷道:“你一定又在底层偷偷搞副作用(Side Effects)了!”
React 睁大眼睛说:“你怎么这样凭空污人清白……”
“什么清白?我前天亲眼见你的 useEffect 里异步网络请求满天飞,闭包陷阱套着闭包陷阱,连个最新状态都拿不到,还在 commit 阶段偷偷改 Ref,被全网吊着打。”
React 便涨红了脸,额上的青筋条条绽出,争辩道:“异步……异步的事怎么能叫副作用呢!……那叫代数效应(Algebraic Effects)!React 调度器里的事,能算不纯么?”接连便是些难懂的话,什么“Fiber 架构”,什么“并发渲染(Concurrent)”,什么“UI 是状态的纯函数映射”,引得整个 Web 社区内外充满了快活的空气
你明明一身历史包袱,底层 DOM 突变和时序补丁糊了一层又一层,还要死死捂着那件打满补丁的「函数式编程」长衫装清高。你连业务代码里最基本的异步抓取和状态流转都做不到开箱即用,还天天搁这儿给开发者念经,说什么“要保持纯洁,要无副作用”
既然你端着全球最大前端基建的架子,那我就得用配得上你这份傲慢的严苛标准来伺候你。我不听你那套自欺欺人的八股文,也不陪你玩“心智模型”的文字游戏。我只负责把你剥个精光,拿着放大镜逐行扒开你 ReactFiberCommitWork.js 的源码,把你装死关掉的 Issue 挨个掘出来
所有写过 React 的人都遇到过这种事:
你写了一个聊天室组件,连接成功后要弹个提示,提示要用当前的主题色 theme
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(roomId)
connection.on('connected', () => {
showToast('连接成功!', theme) // 这里用到了 theme
})
connection.connect()
return connection.disconnect
// ...
}, [roomId, theme]) // 🚨 噩梦来了:React 逼你把 theme 加进依赖数组
}
问题在哪?
你只是想在弹窗时读取一下最新的颜色(theme),但因为 React 的闭包机制,你被迫把 theme 写进依赖数组。
结果就是:用户随便切个暗黑模式(theme 变了),你的聊天室就会断开重连一次。 这简直是灾难
React 在 19.2 给出了 useEffectEvent:把「需要最新值、但不想加依赖」的逻辑包起来
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showToast('连接成功!', theme) // 这里总能拿到最新的 theme
})
useEffect(() => {
const connection = createConnection(roomId)
connection.on('connected', () => {
onConnected()
})
connection.connect()
}, [roomId]) // 🎉 roomId 变了才重连,theme 终于不用加进依赖了
}
听起来是个好东西
React 给这个 API 加了一条硬性限制:
"A function wrapped in useEffectEvent can't be called during rendering."
也就是说:这个函数绝对不准在组件渲染过程中调用,只能在 Effect 或者事件里调用。 如果你把它传给子组件,子组件在 render 里调了一下,整个应用直接报错白屏
为什么限制这么死?
源码里,这个函数的最新值是在 DOM 渲染完之后的 commit 阶段 才挂到内部 ref 上的(见 ReactFiberCommitWork.js)。渲染的时候它还是个空壳或旧值。官方解决不了这时序问题,于是粗暴地用报错来阻止开发者
相比之下社区是怎么做的?
社区早就用 useRef 自己封了一个 useLatestCallback(或等价物):包装函数引用稳定,调用时总是执行当前 ref 里的最新函数
没有任何调用时机的限制,想在哪调在哪调 —— render、effect、事件、传给子组件,都不会因为「during rendering」被拦
官方无视社区极其好用的作业不抄,偏要自己造一个内部实现极其拧巴、强加各种规则、心智负担极重的 useEffectEvent。这就是设计拧巴,文档更拧巴
引用:
ReactFiberHooks.js(useEffectEventImpl、isInvalidExecutionContextForEventFunction)、ReactFiberCommitWork.js(commit 时 ref.impl = nextImpl)一句话:Prepack 是 Meta(Facebook)在 2017 年左右搞的一个失败的 JavaScript 编译器项目
当时 Facebook 异想天开:既然 JS 运行慢,能不能在打包编译的时候就把能算出来的代码提前算好?
比如 let a = 1 + 2 在编译时直接变成 let a = 3。后来他们试图把 Prepack 用在 React 上,想在编译期把组件提前「折叠」优化
但 JavaScript 太动态了,这个饼根本画不圆,项目黄了,被官方放弃
为什么文章里要提它?
因为 React 团队对「编译器」有一种病态的执念
当社区都在拥抱 Signal(用运行时的细粒度响应式解决性能问题)时,React 偏不。他们觉得:当年 Prepack 虽然失败了,但我现在搞个缩小版叫 React Compiler,继续搞编译期魔法
提 Prepack 就是为了扒皮:React 宁可在「编译期优化」这条曾走过弯路的老树上吊死,也不肯听社区的意见去换一套更先进的响应式模型(Signal)
面对依赖数组带来的灾难,社区呼吁引入 Signal 这种现代化的细粒度响应式。React 选了啥?选了他们当年失败过的「编译期优化」老路
从当年胎死腹中的 Prepack,到如今的 React Compiler,官方展现出一种惊人的技术执念:
我们宁可造一个巨型编译器来强行分析依赖、强行插入缓存,也绝不承认「组件级渲染 + 依赖数组」这个底层模型本身已经落后了
这不叫优雅的工程演进,这叫为了掩盖第一代屎山的恶臭,强行喷香水
引用:
GitHub #27164("feature: make react more reactive (feedback for future)"):
GitHub #31393("React Why Not Consider Support Signals"):
身为被全世界当基建的库,对「为什么不做 Signal」这种级别的讨论,不在 issue / 博客 / RFC 里留下可检索的、负责任的说明,而是用「去听我们某次 Conf」打发,这是对社区反馈的轻慢,也是技术傲慢
其实证明 React 底层不仅能上 Signal、而且能上得极其优雅的铁证,早就摆在那里了
社区掏出的 Preact Signals,直接把运行时细粒度更新的满分作业拍在了官方脸上。它完美兼容现有的组件模型,直接证明了闭包陷阱完全可以靠一套现代化的响应式机制来根治
最打脸的是什么?人家一个第三方库,在根本碰不到你 React 核心源码的情况下,都能靠外挂把这套机制跑得明明白白。而你官方握着 Fiber 调度器的生杀大权,却选择视而不见,死活说“做不了”
只能说 React 团队这几年确实是写编译器写魔怔了。简单的运行时解法他们不屑于做,正路不走,非得拉坨大的出来,好像不搞个重型编译链就配不上大厂的 KPI 一样
更可笑的是,如果你现在想用 Preact Signals,你会发现它跟官方硬推的 React Compiler 是直接冲突的
为什么冲突?因为 React Compiler 根本就不是什么优雅的架构演进,它本质上就是一个极其自负的 AST 爆改插件。你原本干干净净的代码,被它过一遍,AST 树上全是被它强行塞进去的 useMemo 和缓存标记,代码执行轨迹完全成了一个黑盒
搞得这么抽象,不知道的还以为你搁这做 JIT 呢
(如果你也受够了官方这种强行喂屎的黑盒操作,想看看怎么在 React 屎山里自救,详见我踩坑两年写出的血泪总结:《花了两年用遍了 React 所有状态管理库,我选出了最现代化的 Signal 方案》)
引用:
从公开信息能拼出的「为什么是 Compiler 而不是 Signal」大致是:
但这些没有在任何官方博客、RFC 或上述 issue 里被系统写出来
选型结果就是:Compiler + 更多工具链;对 Signal 的态度是:不计划、不接题、关 issue 时指到 Conf 视频
技术选型存在,但解释不透明;社区问「为啥不 Signal」得不到可检索的、负责任的答复——这就是技术傲慢:我们怎么做你们就怎么用,理由你们自己找
我可以不用 React,但躲不开
总有人问我:“既然你这么懂,自己封装一套解法不就行了?干嘛天天逮着骂?”
说实话,React 这一地鸡毛的闭包陷阱、渲染地狱,我早就摸透了,甚至有极其成熟的解法和替代方案。但这不代表我觉得它合理!这种开发模式简直蠢透了! 我一个做业务开发的,凭什么要天天搁这儿给框架擦屁股?
前端圈一直有股令人作呕的风气:“React 孝子”们看不起 Vue 等其它框架。他们把「用 React」当成政治正确,把对 React 的批评当成异端。结果就是烂设计没人敢往死里骂,屎山越堆越高
它不仅折磨老手,更是在新人面前砌起了一堵叹息之墙。新人满怀热情想画个交互,结果光是搞懂 useEffect 为什么会死循环、定时器里的 State 为什么永远停留在上个世纪,就得先脱两层皮。 硬生生把一个前端门槛搞得如此畸形、反直觉,官方非但不反思,那一群孝子反而把这种极高的心智负担当成“技术深度”四处炫耀。把喂屎包装成“最佳实践”,这就是你们引以为傲的工程化?
进入 AI 时代,这场灾难彻底演变成了赛博瘟疫。全网投喂的语料导致现在的 AI 写前端默认就是 React,十有八九是 JSX + hooks
我现在日常开发主要靠 AI 写代码,但面对 React 这个奇葩,即便我在 Prompt 和工作流里写了上百行的防坑铁律严防死守,AI 还是会时不时被 React 那套反人类的阴间规则绕晕,悄无声息地给你拉一坨极其隐蔽的屎。 到头来,我还得停下手中的活,亲自下场 Debug,拨开那一层层令人窒息的依赖数组,去查看到底是底层哪个 Hook 又在发癫。这不是在写代码,这是在做赛博排雷。整个 Web 社区的代码基建正在不可逆转地走向失控的屎山化
你以为你不用 React 就能独善其身?直到有一天,你接一个核心的 SDK,点开文档一看:对不起,只有 React 版本。 为了用这一个组件,你被迫在项目里引入整套 React 运行时,被迫去吃那一套恶心的 Hooks 闭包。这不叫技术选型,这叫强买强卖的生态流氓
所以,你问我为什么逮着 React 骂? 因为他早已不是一个你可以躲开的工具,而是一场避无可避的生态瘟疫。骂 React,不是在骂「一个你可以不用的库」,而是在骂已经失控的基建
狂热粉丝只会告诉你 useEffectEvent 怎么用,而我会翻出 ReactFiberCommitWork.js 的源码告诉你它为什么这么难用;小白面对被关掉的 Issue 只会觉得是自己提错了,而我会顺着线索找到那场企图搪塞一切的 React Conf 视频
我拿着所有的官方日志、源码和 Issue 链接站在这里,指着这些拧巴的设计说:作为基建,你现在的傲慢、闭门造车和对社区声音的冷处理,真的很难看
这不是毫无逻辑的狂喷,而是学霸拿着满分试卷在教训连及格线都没达到的出题人—— 用详实的论据、严密的逻辑和底层的代码把你锤得体无完肤
| 类型 | 内容 | 链接 |
|---|---|---|
| 官方博客 | React 19.2 发布(Activity, useEffectEvent, cacheSignal 等) | react-19-2 |
| 官方博客 | React Compiler 1.0 | react-compiler-1 |
| 官方博客 | React Labs: View Transitions, Activity, Automatic Effect Dependencies | react-labs |
| 官方文档 | useEffectEvent Reference | useEffectEvent |
| GitHub | #27164 – make react more reactive / implement signals(无官方回复,stale 关闭) | #27164 |
| GitHub | #31393 – Why Not Consider Support Signals(Joseph Savona 回复 Conf 链接后关闭) | #31393 |
| 源码 | ReactFiberHooks.js(useEffectEventImpl, 渲染期禁止调用) | ReactFiberHooks.js |
| 源码 | ReactFiberCommitWork.js(commit 阶段更新 effect event ref.impl) | ReactFiberCommitWork.js |
| 官方 | Joseph Savona 在 #31393 中指向的 React Conf performance 演讲 | React Conf performance |